export { default as EventChannel } from 'async-event-channel'
import { placeComponents_list } from '@/utils/tools'
import type { Component, ComponentOptionsMixin } from 'vue'
import { routesHistory, backRedirect } from '@/utils/routesHistory'
import router from '@/router'

declare global {
  type EventChannelType = import('async-event-channel').default
}

/**
 * 设置svg颜色
 * @param icon 图标
 * @param param1 配置
 * @param param1.color 颜色
 * @param param1.format 返回的格式
 * @param param1.formatter 格式化函数
 * @returns 设置后的图标
 * @example
 *  import svg from '@/assets/svg/line-tooltip.svg?raw'
 *  const iconFile = setSvgColor(svg, '#fff')
 */
export const setSvgColor = (() => {
  const reg_get = /<svg[^>]*>([\s\S]*)<\/svg>/
  const reg_width = /(<svg[^>]*)width="[^"]+"/
  const reg_height = /(<svg[^>]*)height="[^"]+"/
  const reg_color = /currentColor/g
  return (
    icon = '',
    {
      color = 'currentColor',
      format = 'raw',
      formatter,
    }: {
      color?: string
      format?: 'raw' | 'dataURI'
      formatter?: (value: string) => string
    } = {},
  ) => {
    let value =
      icon
        .match(reg_get)?.[0]
        .replace(reg_width, '$1')
        .replace(reg_height, '$1')
        .replace(reg_color, color) || ''
    if (formatter) {
      value = formatter(value)
    }
    if (format === 'raw') {
      return value
    }
    if (format === 'dataURI') {
      return `data:image/svg+xml,${encodeURIComponent(value)}`
    }
  }
})()

/**
 * px转vw
 * @param px 要转换的px值
 * @param unit 是否带单位
 * @returns 转换后的vw值
 */
export const pxToVw = <T extends boolean = true>(px: number | `${number}px`, unit = true as T) => {
  if (typeof px === 'string') {
    px = Number(px.replace('px', ''))
  }
  const num = px / (Number(__ENV__.VITE_ROOT_DIR) / 100)
  return (unit ? `${num}vw` : num) as T extends true ? `${number}vw` : number
}

/**
 * 设计稿px转真实px
 * @param px 要转换的px值
 * @param unit 是否带单位
 * @returns 转换后的真实px值
 */
export const pxToPX = <T extends boolean = false>(
  px: number | `${number}px`,
  unit = false as T,
) => {
  if (typeof px === 'string') {
    px = Number(px.replace('px', ''))
  }
  const num = (px * window.innerWidth) / Number(__ENV__.VITE_ROOT_DIR)
  return (unit ? `${num}px` : num) as T extends true ? `${number}px` : number
}

/**
 * async/await语法中使用finally
 * @example await promiseFinally(promise, () => console.log('finally'))
 */
export const promiseFinally = <T>(
  promise: Promise<T>,
  onFinally: Parameters<Promise<T>['finally']>[0],
) => {
  promise.finally(onFinally)
  return promise
}

/**
 * promise加载状态
 * @param loading 加载状态
 * @param promise 目标promise
 */
export const promiseLoading = <T>(loading: Ref<boolean>, promise: Promise<T>) => {
  loading.value = true
  return promiseFinally(promise, () => (loading.value = false))
}

/**
 * 组件生命周期-重新激活
 * @param cb 回调函数
 */
export const onReactivate = (cb: () => void) => {
  let one = true
  onActivated(() => {
    if (one) {
      one = false
    } else {
      cb()
    }
  })
}

/**
 * 组件是否激活
 * @returns 是否激活的ref
 */
export const useActivate = () => {
  const activate = ref(true)
  onActivated(() => (activate.value = true))
  onDeactivated(() => (activate.value = false))
  return activate
}

/**
 * 替代scrollIntoView（元素滚动到屏幕内）
 * @see scrollIntoView有时候会让意外的滚动父级滚动，有时候会滚动失败
 * @param param0 配置
 * @param param0.scroll 滚动父级元素
 * @param param0.target 目标元素
 * @param param0.offset 滚动偏移量
 * @param param0.direction 滚动方向
 * @param param0.location 对齐方式
 * @param param0.callback 滚动完成回调
 */
export const scrollToView = ({
  scroll,
  target,
  offset = 0,
  direction = 'horizontal',
  location = 'center',
  callback,
}: {
  scroll: HTMLElement
  target: HTMLElement
  offset?: number
  direction?: 'horizontal' | 'vertical'
  location?: 'start' | 'center' | 'end' | 'nearest'
  callback?: () => void
}) => {
  const isHorizontal = direction === 'horizontal'
  const scrollNum = scroll[isHorizontal ? 'scrollLeft' : 'scrollTop']
  const scrollSize = scroll[isHorizontal ? 'clientWidth' : 'clientHeight']
  const scrollMax = scroll[isHorizontal ? 'scrollWidth' : 'scrollHeight'] - scrollSize
  const targetOffset = target[isHorizontal ? 'offsetLeft' : 'offsetTop']
  const targetSize = target[isHorizontal ? 'offsetWidth' : 'offsetHeight']
  let num: number
  const map = {
    start: targetOffset - offset,
    center: targetOffset - (scrollSize - targetSize) / 2 - offset,
    end: targetOffset - scrollSize + targetSize + offset,
  }
  if (location === 'nearest') {
    if (offset * 2 + targetSize > scrollSize) {
      num = map.center
    } else if (scrollNum > map.start) {
      num = map.start
    } else if (scrollNum < map.end) {
      num = map.end
    } else {
      callback?.()
      return
    }
  } else {
    num = map[location]
  }
  if (num < 0) num = 0
  if (num === scrollNum || (num > scrollNum && scrollNum === scrollMax)) {
    callback?.()
    return
  }
  scroll.scrollTo({
    [isHorizontal ? 'left' : 'top']: num,
    behavior: 'smooth',
  })
  if (callback) {
    scroll.addEventListener('scrollend', callback, { once: true })
  }
}

/**
 * 使用指令式组件
 * @param component 组件的构造函数
 * @param tsxProps 组件的VNodeProps（参考vue3的tsx语法）；排除onPlaceUnmount
 */
export const useDirectiveFunction = (component: Component, tsxProps?: ComponentOptionsMixin) => {
  const item = { component, tsxProps }
  placeComponents_list.push(item)
  return () => placeComponents_list.splice(placeComponents_list.indexOf(item), 1)
}

/**
 * 指令式组件是否存在放置列表
 * @param component 组件的构造函数
 * @returns 是否存在
 */
export const hasDirectiveFunction = (component: Component) =>
  placeComponents_list.some((e) => e.component === component)

/**
 * 单线程函数
 * @see 用于防止同步异步函数在执行时被二次调用
 */
export const useSingleThread = <T extends (...args: any[]) => any>(fn: T) => {
  let lock = false
  return function (this: unknown, ...args) {
    if (lock) return
    lock = true
    const res = fn.apply(this, args)
    if (typeof res?.then === 'function') {
      res.then(
        () => (lock = false),
        () => (lock = false),
      )
    } else {
      lock = false
    }
    return res
  } as T & (() => void)
}

/**
 * Promise防抖
 * @see 防抖后的Promise会保留最后一次的结果
 * @param promise 要防抖的Promise
 * @returns 防抖后的Promise
 */
export const usePromiseDebounce = () => {
  let key: symbol
  return <T>(promise: Promise<T>) =>
    new Promise<T>((resolve, reject) => {
      const _key = (key = Symbol())
      const end = (cb: (res: T) => void) => (res: T) =>
        _key === key ? cb(res) : reject('PromiseDebounce: canceled')
      promise.then(end(resolve), end(reject))
    })
}

/**
 * 获取文字在chart中的宽高-svg渲染器
 * @param text 要计算的文字
 * @param style 文字样式
 * @returns 文字的宽高
 */
export const getChartTextRect = (() => {
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
  svg.setAttribute(
    'style',
    `
    width: max-content;
    height: max-content;
    position: fixed;
    visibility: hidden;
    pointer-events: none;
    `,
  )
  document.documentElement.appendChild(svg)
  return (text: string | number, style: SVGTextElement['style']) => {
    const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text')
    Object.assign(textEl.style, style)
    textEl.textContent = String(text)
    svg.appendChild(textEl)
    const { width, height } = textEl.getBBox()
    svg.removeChild(textEl)
    return { width, height }
  }
})()

/**
 * 校验是否为数字
 * @param val 需要校验的值
 * @returns 校验结果
 */
export const isNumeric = (value: unknown) =>
  !isNaN(parseFloat(value as string)) && isFinite(value as number)

/**
 * 字符串转数字
 * @param str 要转换的字符串
 * @returns 转换后的数字
 */
export const stringToNumber = (value: unknown) => (isNumeric(value) ? Number(value) : null)

/**
 * 判断数字是否小于指定值
 * @param value 需要判断的数字
 * @param target 指定值
 * @returns 是否小于
 */
export const isLessThan = (value: unknown, target: unknown = 0) => {
  if (isNumeric(value) && isNumeric(target)) {
    return Number(value) < Number(target)
  }
}

/**
 * 返回重定向到指定页面
 * @param to 重定向到的页面
 * @param delta 返回的层级，默认为最后一层
 */
export function backRedirectTo(
  to: NonNullable<typeof backRedirect.value>,
  delta = 1 - routesHistory.length,
) {
  if (delta >= 0) {
    router.replace(to)
  } else {
    delta = Math.max(delta, 1 - routesHistory.length)
    backRedirect.value = to
    router.go(delta)
  }
}
